package org.test4j.mock.faking.util;

import g_asm.org.objectweb.asm.ClassReader;

import java.io.*;
import java.lang.reflect.Proxy;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

import static org.test4j.mock.faking.util.ReflectUtility.doThrow;

/**
 * class 文件读取
 *
 * @author darui.wu
 */
public final class ClassFile {

    private ClassFile() {
    }

    /**
     * class 是Object.class 或 Proxy.class
     *
     * @param aClass
     * @return
     */
    public static boolean notObjectOrProxy(Class aClass) {
        return aClass != null && !Objects.equals(aClass, Object.class) && !Objects.equals(aClass, Proxy.class);
    }

    private static byte[] readBytesFromClassFile(String classDesc) {
        return readBytes(classDesc);
    }

    public static byte[] readBytesFromClassFile(Class aClass) {
        String classDesc = TypeUtility.classPath(aClass);
        return readBytesFromClassFile(classDesc);
    }

    public static ClassReader getClassReader(Class clazz) {
        byte[] bytes = ClassFile.readBytes(TypeUtility.classPath(clazz));
        return new ClassReader(bytes);
    }

    public static byte[] readBytes(String classDesc) {
        try (InputStream is = getClassInputStream(classDesc)) {
            byte[] bytecode = new byte[is.available()];
            int len = 0;

            while (true) {
                int n = is.read(bytecode, len, bytecode.length - len);
                if (n == -1) {
                    if (len < bytecode.length) {
                        byte[] truncatedCopy = new byte[len];
                        System.arraycopy(bytecode, 0, truncatedCopy, 0, len);
                        bytecode = truncatedCopy;
                    }
                    return bytecode;
                }

                len += n;
                if (len == bytecode.length) {
                    int last = is.read();
                    if (last < 0) {
                        return bytecode;
                    }
                    byte[] lengthenedCopy = new byte[bytecode.length + 1000];
                    System.arraycopy(bytecode, 0, lengthenedCopy, 0, len);

                    lengthenedCopy[len++] = (byte) last;
                    bytecode = lengthenedCopy;
                }
            }
        } catch (IOException e) {
            throw new RuntimeException("Failed to read class file for " + classDesc.replace('/', '.'), e);
        }
    }

    /**
     * 返回Class文件流
     *
     * @param classDesc
     * @return
     */
    private static InputStream getClassInputStream(String classDesc) {
        String classFileName = classDesc + ".class";
        InputStream inputStream = ClassLoad.CLASS_LOADER.getResourceAsStream(classFileName);
        if (inputStream != null) {
            return inputStream;
        } else {
            throw new NotFoundException(classDesc);
        }
    }

    public static final class NotFoundException extends RuntimeException {
        private NotFoundException(String classNameOrDesc) {
            super("Unable to find class file for " + classNameOrDesc.replace('/', '.'));
        }
    }

    private final static Set<String> Print_Fakes = new HashSet<>();

    /**
     * 设置需要输出asm文件的类
     */
    public static void initPrintFakes() {
        String fakes = System.getProperty("PrintFake");
        if (fakes == null || fakes.trim().isEmpty()) {
            return;
        }
        String[] items = fakes.split("[,;]");
        for (String item : items) {
            if (item == null || item.trim().isEmpty()) {
                continue;
            }
            Print_Fakes.add(item.trim());
        }
    }

    /**
     * 写字节码到文件中, 供测试查看
     *
     * @param fakeClass
     * @param bytes
     */
    public static void writeBytes4Debug(String fakeClass, byte[] bytes) {
        if (!needPrintFakeFile(fakeClass)) {
            return;
        }
        String fileName = System.getProperty("user.dir")
            + "/target/mock/"
            + fakeClass.replace('.', '/').replace('$', '.')
            + ".class";
        File file = new File(fileName);
        if (!file.getParentFile().exists()) {
            file.getParentFile().mkdirs();
        }
        try (OutputStream out = new FileOutputStream(file);
             InputStream is = new ByteArrayInputStream(bytes)) {
            byte[] buff = new byte[1024];
            int len = 0;
            while ((len = is.read(buff)) != -1) {
                out.write(buff, 0, len);
            }
        } catch (Exception e) {
            doThrow(e);
        }
    }

    private static boolean needPrintFakeFile(String fakeClass) {
        if (Print_Fakes.isEmpty()) {
            return false;
        }
        for (String namePart : Print_Fakes) {
            if (fakeClass.contains(namePart)) {
                return true;
            }
        }
        return false;
    }
}